/*
 * $Id: sttp.c,v 1.27 2006-01-30 16:31:06 bacon Exp $
 */

#include <xp/net/sttp.h>
#include <xp/bas/memory.h>
#include <xp/bas/ctype.h>
#include <xp/bas/stdarg.h>
#include <xp/bas/utf8.h> /* sttp only supports utf-8 */
#include <xp/bas/errno.h>

static int get_char (xp_sttp_t* sttp, xp_cint_t* ch);
static int get_token (xp_sttp_t* sttp, xp_bool_t first);
static int get_ident (xp_sttp_t* sttp);
static int get_string (xp_sttp_t* sttp);
static xp_cint_t trans_escaped_char (xp_cint_t c);
static int is_ident_char (xp_cint_t c);
static int put_char (xp_sttp_t* sttp, xp_cint_t c);
static int put_char_ne (xp_sttp_t* sttp, xp_cint_t c);
static int put_mchar (xp_sttp_t* sttp, xp_mchar_t ch);
static int flush_outbuf (xp_sttp_t* sttp);

#define GET_CHAR(sttp) if (get_char(sttp,XP_NULL) == -1) return -1;
#define GET_TOKEN(sttp) if (get_token(sttp,xp_false) == -1) return -1;
#define GET_FIRST_TOKEN(sttp) if (get_token(sttp,xp_true) == -1) return -1;
#define PUT_CHAR(sttp,x) if (put_char(sttp,x) == -1) return -1;
#define PUT_ENC_CHAR(x) if (put_char_ne(sttp,x) == -1) return -1;

enum {
	MAX_RAW_CMD_LEN = 4096,
	MAX_ARG_COUNT = 20
};

enum {
	T_STRING      = 1,
	T_IDENT       = 2,
	T_SEMICOLON   = 3,
	T_COMMA       = 4,
	T_ENC         = 5
};

#define MARK_ENC XP_CHAR('!')

xp_sttp_t* xp_sttp_open (xp_sttp_t* sttp, xp_sckhnd_t handle)
{

	if (sttp == XP_NULL) {
		sttp = (xp_sttp_t*)xp_malloc (xp_sizeof(xp_sttp_t));
		if (sttp == XP_NULL) return XP_NULL;
		sttp->__dynamic = xp_true;
	}
	else sttp->__dynamic = xp_false;

	sttp->handle = handle;

	sttp->recv_timeout = -1;
	sttp->send_timeout = -1;

	sttp->max_raw_cmd_len = MAX_RAW_CMD_LEN;
	sttp->max_arg_count = MAX_ARG_COUNT;
	sttp->raw_cmd_len = 0;

	sttp->opt_enc_always = xp_false;
	sttp->opt_enc_send = xp_false;
	sttp->opt_enc_recv = xp_false;
	sttp->opt_send_newline = xp_true;

	xp_rc4_open (&sttp->rc4_recv);
	xp_rc4_open (&sttp->rc4_send);

	sttp->inbuf_len  = 0;
	sttp->outbuf_len = 0;
	sttp->curp       = 0;
	sttp->curc       = XP_CHAR('\0');

	if (xp_str_open (&sttp->token_value, 256) == XP_NULL) {
		if (sttp->__dynamic) xp_free (sttp);
		return NULL;
	}

	return sttp;
}

void xp_sttp_close (xp_sttp_t* sttp)
{
	xp_str_close (&sttp->token_value);

	xp_rc4_close (&sttp->rc4_send);
	xp_rc4_close (&sttp->rc4_recv);
	//xp_sckclose (sttp->handle);
	if (sttp->__dynamic) xp_free (sttp);
}

int xp_sttp_recv (xp_sttp_t* sttp, xp_str_t* cmd, xp_size_t* count)
{
	xp_size_t arg_count = 0;

	sttp->errnum = XP_STTP_ENONE;

	sttp->cancel_recv = xp_false;
	sttp->raw_cmd_len = 0;
	if (!sttp->opt_enc_always) sttp->opt_enc_recv = xp_false;

	GET_CHAR (sttp);
	GET_FIRST_TOKEN (sttp);

	/*
	if (sttp->token_type == T_SEMICOLON) { 
		// null command
		cmd->clear ();
		return 0;
	}
	*/

	if (sttp->token_type == T_ENC) { GET_TOKEN (sttp); }

	if (sttp->token_type != T_IDENT) {
		sttp->errnum = XP_STTP_ECMDNAME;
		return -1;
	}

	xp_str_clear (cmd);
	if (xp_str_ncat (cmd, 
		(const xp_char_t*)&sttp->token_value.size,
// TOOD: this code may not work at all depending on the size of size_t
// TOOD: rewrite sttp.c entirely.
// TODO: better buffer management... especiall the size....
		xp_sizeof(sttp->token_value.size) / xp_sizeof(xp_char_t)) == (xp_size_t)-1) {
		sttp->errnum = XP_STTP_ENOMEM;
		return -1;
	}
	if (xp_str_ncat (cmd, 
		sttp->token_value.buf, 
		sttp->token_value.size + 1) == (xp_size_t)-1) {
		sttp->errnum = XP_STTP_ENOMEM;
		return -1;
	}

	for (;;) {
		GET_TOKEN (sttp);
		if (sttp->token_type == T_STRING) {
			if (xp_str_ncat (cmd, 
				(const xp_char_t*)&sttp->token_value.size,
				xp_sizeof(sttp->token_value.size) / xp_sizeof(xp_char_t)) == (xp_size_t)-1) {
				sttp->errnum = XP_STTP_ENOMEM;
				return -1;
			}
			if (xp_str_ncat (cmd, 
				sttp->token_value.buf,
				sttp->token_value.size + 1) == (xp_size_t)-1) {
				sttp->errnum = XP_STTP_ENOMEM;
				return -1;
			}
			arg_count++;
		}
		else if (sttp->token_type == T_IDENT) {
			if (xp_str_ncat (cmd,
				(const xp_char_t*)&sttp->token_value.size,
				xp_sizeof(sttp->token_value.size)/ xp_sizeof(xp_char_t)) == (xp_size_t)-1) {
				sttp->errnum = XP_STTP_ENOMEM;
				return -1;
			}
			if (xp_str_ncat (cmd, 
				sttp->token_value.buf,
				sttp->token_value.size + 1) == (xp_size_t)-1) {
				sttp->errnum = XP_STTP_ENOMEM;
				return -1;
			}
			arg_count++;
		}
		else {
			if (arg_count == 0) break;
			sttp->errnum = XP_STTP_EWRONGARG;
			return -1;
		}

		GET_TOKEN (sttp);
		if (sttp->token_type != T_COMMA) break;
		if (arg_count >= sttp->max_arg_count) {
			sttp->errnum = XP_STTP_ETOOMANYARGS;
			return -1;
		}
	}

	if (sttp->token_type != T_SEMICOLON) {
		sttp->errnum = XP_STTP_ESEMICOLON;
		return -1;
	}

	*count = arg_count + 1;
	return 0;
}

int xp_sttp_send (xp_sttp_t* sttp, const xp_char_t* name, xp_size_t nargs, ...)
{
	const xp_char_t* p = name;

	sttp->errnum = XP_STTP_ENONE;
	if (*p == XP_CHAR('\0')) return 0; // don't send a null command

	if (!sttp->opt_enc_always && sttp->opt_enc_send) {
		PUT_ENC_CHAR(MARK_ENC);	
		xp_rc4_restart (&sttp->rc4_send);
	}
	while (*p != XP_CHAR('\0')) PUT_CHAR (sttp, *p++);

	if (nargs > 0) {
		xp_size_t i;
		xp_va_list ap;
		xp_va_start (ap, nargs);

		PUT_CHAR (sttp, XP_CHAR(' '));
		for (i = 1; i <= nargs; i++) {
			p = xp_va_arg (ap, xp_char_t*);

			PUT_CHAR (sttp, XP_CHAR('\"'));
			while (*p) {
				if (*p == XP_CHAR('\\') || 
				    *p == XP_CHAR('\"')) PUT_CHAR (sttp, XP_CHAR('\\'));
				PUT_CHAR (sttp, *p++); 
			}
			PUT_CHAR (sttp, XP_CHAR('\"'));
	
			if (i < nargs) PUT_CHAR (sttp, XP_CHAR(','));
		}
		xp_va_end (ap);
	}

	PUT_CHAR (sttp, XP_CHAR(';'));
	if (sttp->opt_send_newline) {
		PUT_CHAR (sttp, XP_CHAR('\r'));
		PUT_CHAR (sttp, XP_CHAR('\n'));
	}
	return flush_outbuf (sttp);
}

int xp_sttp_sendn (xp_sttp_t* sttp, 
	const xp_char_t* name, xp_size_t nargs, const xp_char_t* args[])
{
	const xp_char_t* p = name;

	sttp->errnum = XP_STTP_ENONE;
	if (*p == XP_CHAR('\0')) return 0; // don't send a null command

	if (!sttp->opt_enc_always && sttp->opt_enc_send) {
		PUT_ENC_CHAR(MARK_ENC);	
		xp_rc4_restart (&sttp->rc4_send);
	}
	while (*p != XP_CHAR('\0')) PUT_CHAR (sttp, *p++);

	if (nargs > 0) {
		xp_size_t i;

		PUT_CHAR (sttp, XP_CHAR(' '));
		for (i = 1; i <= nargs; i++) {
			p = args[i - 1];

			PUT_CHAR (sttp, XP_CHAR('\"'));
			while (*p) {
				if (*p == XP_CHAR('\\') || 
				    *p == XP_CHAR('\"')) PUT_CHAR (sttp, XP_CHAR('\\'));
				PUT_CHAR (sttp, *p++); 
			}
			PUT_CHAR (sttp, XP_CHAR('\"'));
	
			if (i < nargs) PUT_CHAR (sttp, XP_CHAR(','));
		}
	}

	PUT_CHAR (sttp, XP_CHAR(';'));
	if (sttp->opt_send_newline) {
		PUT_CHAR (sttp, XP_CHAR('\r'));
		PUT_CHAR (sttp, XP_CHAR('\n'));
	}
	return flush_outbuf (sttp);
}

static int get_char (xp_sttp_t* sttp, xp_cint_t* ch)
{
	xp_size_t remain = 0;

#ifdef XP_CHAR_IS_WCHAR
	xp_size_t seqlen;
	xp_size_t i;
	xp_wchar_t wch;
#endif

	if (sttp->curp == sttp->inbuf_len) {
		xp_ssize_t n;
		xp_time_t cnt;
#ifdef XP_CHAR_IS_WCHAR
	get_char_utf8:
#endif
		cnt = 0;
		while (1) {
			if (sttp->cancel_recv) {
				sttp->errnum = XP_STTP_ECANCEL;
				return -1;
			}

			n = xp_sckrecv (sttp->handle, &sttp->inbuf[remain], 
				xp_countof(sttp->inbuf) - remain, 0, 1000);
			if (n != -1 || xp_geterrno() != XP_ETIMEDOUT) break;

			if (sttp->recv_timeout >= 0) {
				cnt += 1000;
				if (cnt >= sttp->recv_timeout) break;
			}
		}

		if (n == -1 && xp_geterrno() == XP_ETIMEDOUT) {
			sttp->errnum = XP_STTP_ETMOUT;
			return -1;
		}
		if (n <= 0) {
			sttp->errnum = XP_STTP_ERECV;
			return -1;
		}

		sttp->curp = 0;
		sttp->inbuf_len = (xp_size_t)n + remain;
	}

#ifdef XP_CHAR_IS_WCHAR
	if ((sttp->opt_enc_always || sttp->opt_enc_recv) && remain == 0) {
		sttp->inbuf[sttp->curp] = xp_rc4_cipher (
			&sttp->rc4_recv, sttp->inbuf[sttp->curp]);
	}

	seqlen = xp_utf8_sequence_len (sttp->inbuf[sttp->curp]);
	if (seqlen == 0) {
		sttp->curp++; // skip one byte
		sttp->errnum = XP_STTP_EILSEQ;
		return -1;
	}

	remain = sttp->inbuf_len - sttp->curp;
	if (remain < seqlen) {
		// must read further...
		xp_memcpy (sttp->inbuf, &sttp->inbuf[sttp->curp], remain);
		sttp->curp = 0;
		sttp->inbuf_len = remain;
		goto get_char_utf8;
	}

	if (sttp->opt_enc_always || sttp->opt_enc_recv) {
		for (i = sttp->curp + 1; i < sttp->curp + seqlen; i++) {
			// only from the second character
			sttp->inbuf[i] = xp_rc4_cipher (
				&sttp->rc4_recv, sttp->inbuf[i]);
		}
	}

	i = xp_utf8_to_unicode (&sttp->inbuf[sttp->curp], seqlen, &wch);
	if (i == 0) {
		sttp->curp++; // still must skip a character
		sttp->errnum = XP_STTP_ERECV;
		return -1;	
	}

	sttp->curc = wch;
	sttp->curp += i;
#else
	if (sttp->opt_enc_always || sttp->opt_enc_recv) {
		sttp->inbuf[sttp->curp] = xp_rc4_cipher (
			&sttp->rc4_recv, sttp->inbuf[sttp->curp]);
	}
	sttp->curc = sttp->inbuf[sttp->curp++];
#endif

	/* 
	if (sttp->curc == XP_CHAR('\0')) {
		sttp->errnum = XP_STTP_EWRONGCHAR;
		return -1;
	}
	*/

	if (sttp->raw_cmd_len >= sttp->max_raw_cmd_len) {
		sttp->errnum = XP_STTP_ETOOLONGCMD;
		return -1;
	}
	sttp->raw_cmd_len++;

	if (ch != XP_NULL) *ch = sttp->curc;
	return 0;
}

static int get_token (xp_sttp_t* sttp, xp_bool_t first)
{
	while (xp_isspace(sttp->curc)) GET_CHAR(sttp); // skip spaces...

	if (is_ident_char(sttp->curc)) return get_ident (sttp);
	else if (sttp->curc == XP_CHAR('\"')) return get_string (sttp);
	else if (sttp->curc == XP_CHAR(';')) {
		sttp->token_type = T_SEMICOLON;
		xp_str_ccpy (&sttp->token_value, XP_CHAR(';'));
		// do not read the next character to terminate a command
		// get_char ();
	}
	else if (sttp->curc == XP_CHAR(',')) {
		sttp->token_type  = T_COMMA;
		xp_str_ccpy (&sttp->token_value, XP_CHAR(','));
		GET_CHAR (sttp);
	}
	else if (!sttp->opt_enc_always && sttp->curc == MARK_ENC) {
		sttp->token_type = T_ENC;
		xp_str_ccpy (&sttp->token_value, MARK_ENC);
		if (first) {
			sttp->opt_enc_recv = xp_true;
			xp_rc4_restart (&sttp->rc4_recv);
		}
		GET_CHAR (sttp);
	}
	else {
		sttp->errnum = XP_STTP_EWRONGCHAR;
		return -1;
	}

	return 0;
}

static int get_ident (xp_sttp_t* sttp)
{
	sttp->token_type  = T_IDENT;
	xp_str_clear (&sttp->token_value);

	while (is_ident_char(sttp->curc)) {
		if (xp_str_ccat(&sttp->token_value, sttp->curc) == (xp_size_t)-1) {
			sttp->errnum = XP_STTP_ENOMEM;
			return -1;
		}
		GET_CHAR(sttp);
	}

	return 0;
}

static int get_string (xp_sttp_t* sttp)
{
	xp_bool_t escaped = xp_false;

	sttp->token_type = T_STRING;
	xp_str_clear (&sttp->token_value);

	GET_CHAR (sttp);
	for (;;) {
		if (escaped) {
			sttp->curc = trans_escaped_char (sttp->curc);
			escaped = xp_false;
		}
		else {
			if (sttp->curc == XP_CHAR('\"')) {
				GET_CHAR (sttp);
				break;
			}
			else if (sttp->curc == XP_CHAR('\\')) {
				GET_CHAR (sttp);
				escaped = xp_true;
				continue;
			}
		}

		if (xp_str_ccat(&sttp->token_value, sttp->curc) == (xp_size_t)-1) {
			sttp->errnum = XP_STTP_ENOMEM;		
			return -1;
		}
		GET_CHAR (sttp);
	}

	return 0;
}

static xp_cint_t trans_escaped_char (xp_cint_t c)
{
	if (c == XP_CHAR('n')) c = XP_CHAR('\n');
	else if (c == XP_CHAR('t')) c = XP_CHAR('\t');
	else if (c == XP_CHAR('r')) c = XP_CHAR('\r');
	else if (c == XP_CHAR('v')) c = XP_CHAR('\v');
	else if (c == XP_CHAR('f')) c = XP_CHAR('\f');
	else if (c == XP_CHAR('a')) c = XP_CHAR('\a');
	else if (c == XP_CHAR('b')) c = XP_CHAR('\b');
	//else if (c == XP_CHAR('0')) c = XP_CHAR('\0');

	return c;
}

static int is_ident_char (xp_cint_t c)
{
	return xp_isalnum(c) || c == XP_CHAR('_') || 
		c == XP_CHAR('.') || c == XP_CHAR('*') || c == XP_CHAR('@');
}

static int put_char (xp_sttp_t* sttp, xp_cint_t c)
{
#ifdef XP_CHAR_IS_WCHAR
	xp_mchar_t buf[10];
	xp_size_t i;

	xp_size_t len = xp_unicode_to_utf8 (c, buf, xp_countof(buf));
	if (len == 0) {
		sttp->errnum = XP_STTP_EILSEQ;
		return -1;
	}

	for (i = 0; i < len; i++) {
		if (sttp->opt_enc_always || sttp->opt_enc_send) {
			buf[i] = xp_rc4_cipher (&sttp->rc4_send, buf[i]);
		}
		if (put_mchar(sttp, buf[i]) == -1) return -1;
	}

	return 0;
#else
	if (sttp->opt_enc_always || sttp->opt_enc_send) {
		c = xp_rc4_cipher (&sttp->rc4_send, c);
	}

	return put_mchar (sttp, c);
#endif
}

static int put_char_ne (xp_sttp_t* sttp, xp_cint_t c)
{
#ifdef XP_CHAR_IS_WCHAR
	xp_size_t i;
	xp_mchar_t buf[10];

	xp_size_t len = xp_unicode_to_utf8 (c, buf, xp_countof(buf));
	if (len == 0) {
		sttp->errnum = XP_STTP_EILSEQ;
		return -1;
	}

	for (i = 0; i < len; i++) {
		if (put_mchar(sttp, buf[i]) == -1) return -1;
	}

	return 0;
#else
	return put_mchar (sttp, c);
#endif
}

static int put_mchar (xp_sttp_t* sttp, xp_mchar_t ch)
{
	if (sttp->outbuf_len >= xp_countof(sttp->outbuf)) {
		/* NOTICE:
		 *   if a previous flush operation has failed when the buffer
		 *   was full, the buffer will still be full in the next call
		 *   to put_mchar.
		 */
		sttp->errnum = XP_STTP_ENOMEM;
		return -1;
	}

	sttp->outbuf[sttp->outbuf_len++] = ch;
	if (sttp->outbuf_len >= xp_countof(sttp->outbuf)) 
		return flush_outbuf(sttp);
	return 0;
}

static int flush_outbuf (xp_sttp_t* sttp)
{
	if (sttp->outbuf_len > 0) {
		xp_ssize_t n;

		n = xp_scksendall (sttp->handle, 
			sttp->outbuf, sttp->outbuf_len, 0, sttp->send_timeout);
		if (n == -1 && xp_geterrno() == XP_ETIMEDOUT) {
			sttp->errnum = XP_STTP_ETMOUT;
			return -1;
		}

		//if (n <= 0) {
		if (n != (xp_ssize_t)sttp->outbuf_len) {
			sttp->errnum = XP_STTP_ESEND;
			return -1;
		}
 
		sttp->outbuf_len = 0;
	}
 
	return 0;
}

void xp_sttp_cancelrecv (xp_sttp_t* sttp)
{
	sttp->cancel_recv = xp_true;
}
